/*
 * Copyright (c) 2013 Eugene Kuzmenko
 *
 * Permission  is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in  the Software without restriction, including without limitation the rights
 * to  use,  copy,  modify,  merge, publish, distribute, sublicense, and/or sell
 * copies  of  the  Software,  and  to  permit  persons  to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The  above  copyright  notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE  IS  PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED,  INCLUDING  BUT  NOT  LIMITED  TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS  FOR  A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS  OR  COPYRIGHT  HOLDERS  BE  LIABLE  FOR  ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * For more information, see <http://www.aljey.com>.
 */

/**
 * @name _super
 * A magic virtual variable that can be used to call the super method of a
 * class.
 *
 *    _super();
 *    _super.methodName(param);
 *
 * If no methodName is specified, a method with the same name is invoked.
 * If no params are passed, the same params that have been passed to the current
 * method (arguments) are passed instead.
 * Is compiled to:
 *
 *    this.parent.apply(this, arguments);
 *    this.callParent('methodName', [param]);
 *
 * Both notations can be used interchangeably, though the first one is
 * preferred.
 */

/**
 * @name _static
 * A magic virtual variable that can be used to access static properties of a
 * class.
 *
 *    _static.ClassName;
 *    _static.ClassName.methodName();
 *    _static.ClassName.propertyName;
 *    _static.ClassName.propertyName.subProperty;
 *
 * ClassName is required.
 * Is compiled to:
 *
 *    Class.getStatic('ClassName');
 *    Class.getStatic('ClassName.methodName')();
 *    Class.getStatic('ClassName.propertyName');
 *    Class.getStatic('ClassName.propertyName.subProperty');
 *
 * Both notations can be used interchangeably, though the first one is
 * preferred.
 */

/**
 * The base functions for OOP support.
 *
 * @author Eugene Kuzmenko <mail@aljey.com>
 */
(function () {
  var types = {Base: function constructor() {}}, statics = {Base: {}}, ns,
    instances = {}, Base = types['Base'].prototype, Class = window.Class = {};
  if (!_.isFunction(typeof Object.getPrototypeOf)) {
    Object.getPrototypeOf = _.isObject('test'.__proto__) ? function (object) {
      return object.__proto__;
    } : function (object) {
      return object.constructor.prototype;
    };
  }
  function apply(me, options) {
    _.each(options, function (item, i) {
      me[i] = (_.isFunction(item) && _.isFunction(me[i])) ?
        (function (fn, original, _super) {
          return function () {
            var tmp1 = this.parent, tmp2 = this.callParent;
            this.parent = original;
            this.callParent = function (name, args) {
              if (_.isFunction(_super[name])) {
                return _super[name].apply(this, args);
              }
            };
            var r = fn.apply(this, arguments);
            this.parent = tmp1;
            this.callParent = tmp2;
            return r;
          };
        }(item, me[i], types[me.extend || 'Base'].prototype)) : item;
    });
    me.events = _.cloneDeep(me.events || {});
    _.each((options || {}).listeners, function (item, i) {
      me.on(i, item);
    });
  }
  function assert(type) {
    if (!types[type]) {
      throw 'Fatal: Class ' + type + ' is not defined';
    }
  }

  /**
   * Calls an arbitrary method of an arbitrary class and returns the result of
   * that call.
   *
   * @param {string} type Class name.
   * @param {string} method Method name.
   * @param {array} [args] A collection of arguments to pass.
   * @returns {*}
   */
  Base.callMethod = function (type, method, args) {
    assert(type);
    return types[type].prototype[method].apply(this, args || []);
  };

  /**
   * Registers an event listener.
   *
   * @param {string} name Event name.
   * @param {Function} fn A handler function.
   * @param {bool} [once = false] If true can only be fired once.
   * @returns {Base}
   */
  Base.on = function (name, fn, once) {
    if (fn) {
      fn.once = !!once;
      this.events || (this.events = {});
      this.events[name] || (this.events[name] = []);
      _.contains(this.events[name], fn) || this.events[name].unshift(fn);
    }
    return this;
  };

  /**
   * Removes an event listener.
   *
   * @param {string} [name] Event name. If not passed all event listeners for
   *                        all events are going to be removed.
   * @param {Function} [fn] A handler function. If not passed all event
   *                        listeners for a particular event are going to be
   *                        removed.
   * @returns {Base}
   */
  Base.un = function (name, fn) {
    if (!this.events) {
      return this;
    }
    if (arguments.length) {
      if (fn) {
        var i = _.indexOf(this.events[name], fn);
        (-1 === i) || this.events[name].splice(i, 1);
      } else {
        delete this.events[name];
      }
    } else {
      delete this.events;
    }
    return this;
  };

  /**
   * Fires an event.
   *
   * @param {string} name Event name.
   * @param {array} [args] A collection of arguments to pass to the listeners.
   * @returns {boolean} True if this event was not canceled by a listener
   *                    returning false.
   */
  Base.fire = function (name, args) {
    if (this.events && this.events[name]) {
      var i = this.events[name].length;
      while (i--) {
        if (false === this.events[name][i].apply(this, args || [])){
          this.events[name][i].once && this.events[name].splice(i, 1);
          return false;
        }
        this.events[name][i].once && this.events[name].splice(i, 1);
      }
    }
    return true;
  };

  /**
   * Used to destroy objects.
   * Fires a "destroy" event to allow for any additional cleanup prior to
   * self-destructing.
   */
  Base.destroy = function () {
    this.fire('destroy');
    _.forOwn(this, function(num, key) {
      this[key] = null;
    }, this);
  };

  /**
   * Defines a class.
   *
   * @param {string} type A class name.
   * @param {Object} [options] Class members. May contain 2 special properties:
   *                           "init" which is the class constructor and
   *                           "listeners" containing all the event listeners
   *                           that this class needs to have predefined.
   */
  Class.define = function (type, options) {
    options || (options = {});
    options.extend || (options.extend = 'Base');
    assert(options.extend);
    options.type = type;
    options.isAbstract = !!options.isAbstract;
    types[type] = function constructor() {};
    types[type].prototype = new types[options.extend]();
    types[type].prototype.constructor = types[type];
    statics[type] = options.statics || {};
    delete options.statics;
    apply(types[type].prototype, options);
  };

  /**
   * Creates a class instance(s).
   *
   * @param {string|Object|array} type A class name, an object with a "type"
   *                                   property or an array of either.
   * @param {Object} [options] Values to apply to the instance(s).
   * @returns {Base|array} A class instance(s).
   */
  Class.create = function (type, options) {
    var isArray = _.isArray(type), response = [];
    isArray || (type = [type]);
    options || (options = {});
    _.each(type, function (type) {
      if (Class.is('Base', type)) {
        response.push(type);
        return;
      }
      var config = {};
      if (_.isPlainObject(type)) {
        config = type;
        type = config.type;
      }
      _.defaults(config, options);
      assert(type);
      if (types[type].prototype.isAbstract) {
        throw 'Fatal: Class ' + type + ' is abstract';
      }
      if (types[type].prototype.singleton && instances[type]) {
        response.push(instances[type]);
        return;
      }
      var me = new types[type]();
      apply(me, config);
      me.init && me.init();
      me.singleton && (instances[type] = me);
      response.push(me);
    });
    return ((1 === response.length) && !isArray) ? response[0] : response;
  };

  /**
   * Checks whether an object is an instance of a particular class.
   *
   * @param {string} type A class name.
   * @param {*} object
   * @returns {boolean}
   */
  Class.is = function (type, object) {
    assert(type);
    return object instanceof types[type];
  };

  /**
   * Retrieves a static property of a class.
   *
   * @param {String} name A dot separated name (eg. Class.property.value)
   * @returns {*}
   */
  Class.getStatic = function (name) {
    var path = name.split('.'), len = path.length, result;
    assert(path[0]);
    result = statics[path[0]];
    for (var i = 1; i < len; i++) {
      result = result && result[path[i]];
    }
    return result;
  };

}());